"Les cours de neeko.fr"

Retour en haut

Vers la (vraie) programmation orientée objet

Vers la (vraie) P.O.O.

Présentation

Java est un langage "orienté-objet". Pratiquement tous les éléments qu'il manipule sont encapsulés dans des objets.

Cela dit, pour profiter pleinement des avantages de ce paradigme, il convient de respecter un certains nombre de règles.

Les règles de bases sont :

Une application qui fonctionne n'est pas forcement une application terminée.

Une bonne application est une application qui à été pensée par rapport au besoin initial, et qui permet une évolution sereine et maitrisée pour des besoins futurs.

Dans le monde de l'informatique non-industrielle, la seule constante est le changement.

Etude d'une petite application

La demande initiale de cette application est d'afficher, pour un age donné, des informations sur les droits de l'utilisateur.

Pour commencer, on affichera seulement s'il est majeur, et on ajoutera plus tard d'autres fonctionnalités, comme la possibilité de départ à la retraite par exemple, et d'autres informations liée à l'âge.

Soit le simple layout :

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <!-- Le champ permettant de renseigner l'age --> <EditText android:id="@+id/myEdit" android:layout_width="match_parent" android:layout_height="wrap_content" /> <!-- Le bouton de validation --> <Button android:id="@+id/myButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Cliquez !" /> <!-- La zone de texte affichant le resultat --> <TextView android:id="@+id/myText" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="" /> </LinearLayout>

Niveau 0 de l'orienté objet : la classe monolithique

package fr.cnam.premierprogramme; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.app.Activity; public class MainActivity extends Activity implements OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button monButton = (Button) findViewById(R.id.myButton); monButton.setText("Cliquez s'il vous plait !"); monButton.setOnClickListener(this); } @Override public void onClick(View v) { EditText monEdit = (EditText) findViewById(R.id.myEdit); //transformation du texte en "int" int monAge = Integer.parseInt( monEdit.getText().toString() ); TextView monTexte = (TextView) findViewById(R.id.myText); if (monAge >= 18) { monTexte.setText(monAge + " ans, vous etes majeur."); } else { monTexte.setText(monAge + " ans, vous etes mineur."); } } }

Le programme est fonctionnel, mais la seule classe MainActivity regroupe toutes les responsabilités.

Si je dois faire évoluer le programme, pour y ajouter des fonctionnalités, je dois m'intégrer au coeur de mon code.

Par exemple, pour rajouter la fonctionnalité de l'âge de la retraite, en modifiant la fonction onClick :

@Override public void onClick(View v) { EditText monEdit = (EditText) findViewById(R.id.myEdit); //transformation du texte en "int" int monAge = Integer.parseInt( monEdit.getText().toString() ); TextView monTexte = (TextView) findViewById(R.id.myText); String text = "Vous avez " + monAge + " ans, "; if (monAge >= 18) { text = text + " vous etes majeur, "; } else { text = text + " vous etes mineur, "; } if (monAge >= 62) { text = text + " et vous pouvez prendre votre retraite."; } else { text = text + " et vous devez encore travailler, desole."; } monTexte.setText(text); }

Quel est le probleme avec ce mode de fonctionnement ?

Le problème est que la classe MainActivity et plus précisement la fonction onClick centralise toutes les responsabilités de l'application.

Cela peut rendre la compréhension difficile et surtout ne permet pas de répondre à un changement sans risquer de toucher d'autres fonctionnalités : on a + de risques de créer des bugs.

Niveau 1 de l'orienté objet : encapsuler les variations

En créant les 2 classes suivantes :

package fr.cnam.premierprogramme; public class CheckMajorite { public boolean estMajeur(int age){ if (age >= 18) { return true; } else { return false; } } }

package fr.cnam.premierprogramme; public class CheckRetraite { public boolean peutPartirRetraite(int age){ if (age >= 62) { return true; } else { return false; } } }

Et en modifiant la méthode Onclick :

@Override public void onClick(View v) { CheckMajorite majorite = new CheckMajorite(); CheckRetraite retraite = new CheckRetraite(); EditText monEdit = (EditText) findViewById(R.id.myEdit); int monAge = Integer.parseInt( monEdit.getText().toString() ); TextView monTexte = (TextView) findViewById(R.id.myText); String text = "Vous avez " + monAge + " ans."; if (majorite.estMajeur(monAge)) { text = text + " vous etes majeur, "; } else { text = text + " vous etes mineur, "; } if (retraite.peutPartirRetraite(monAge)) { text = text + " et vous pouvez prendre votre retraite."; } else { text = text + " et vous devez encore travailler, desole."; } monTexte.setText(text); }

La fonction onClick est toujours aussi grosse, mais au moins une partie de l'intelligence est extraite.

Une modification sur un des algorithme serait plus aisée (ne toucherai que la classe concernée).

Par contre, on continue d'avoir des difficultés à ajouter une fonctionnalité. En plus, une partie de l'intelligence reste dans cette méthode : le texte à afficher selon le résultat.

On pourrait donc laisser la responsabilité du choix du texte à nos classes "Check..." :

package fr.cnam.premierprogramme; public class CheckMajorite { public String estMajeur(int age){ if (age >= 18) { return " vous etes majeur "; } else { return " vous etes mineur "; } } }

package fr.cnam.premierprogramme; public class CheckRetraite { public String peutPartirRetraite(int age){ if (age >= 62) { return " et vous pouvez prendre votre retraite."; } else { return " et vous devez encore travailler, desole."; } } }

@Override public void onClick(View v) { CheckMajorite majorite = new CheckMajorite(); CheckRetraite retraite = new CheckRetraite(); EditText monEdit = (EditText) findViewById(R.id.myEdit); int monAge = Integer.parseInt( monEdit.getText().toString() ); TextView monTexte = (TextView) findViewById(R.id.myText); String text = "Vous avez " + monAge + " ans."; text += majorite.estMajeur(monAge); text += retraite.peutPartirRetraite(monAge); monTexte.setText(text); }

La lecture de la fonction onClick est nettement clarifiée.

Les responsabilités sont bien identifiées, au niveau de l'intelligence au moins.

Mais : n'y a-il pas des points communs à nos 2 classes CheckMajorite et CheckRetraite ?

Niveau 2 de l'orienté objet : dépendre d'interfaces

Les 2 classes CheckMajorite et CheckRetraite ont le même objectif : à partir d'un âge donné, renvoyer une réponse (sous forme de texte) sur nos droits.

On pourrait donc imaginer les ranger dans une "catégorie" commune. Chaque objet pourrait être appelé de la même manière...

Les interfaces en Java permettent cela : elles permettent de déclarer l'interface d'un objet, son mode d'emploi.

Elle détermine COMMENT on peut accéder à un objet, mais pas ce qu'il fait vraiment. Cela permet d'avoir une abstraction supplémentaire.

package fr.cnam.premierprogramme; public interface CheckAgeInterface { public String check(int age); }

package fr.cnam.premierprogramme; public class CheckMajorite implements CheckAgeInterface { public String check(int age){ if (age >= 18) { return " vous etes majeur "; } else { return " vous etes mineur "; } } }

package fr.cnam.premierprogramme; public class CheckRetraite implements CheckAgeInterface { public String check(int age){ if (age >= 62) { return " et vous pouvez prendre votre retraite."; } else { return " et vous devez encore travailler, desole."; } } }

@Override public void onClick(View v) { CheckMajorite majorite = new CheckMajorite(); CheckRetraite retraite = new CheckRetraite(); EditText monEdit = (EditText) findViewById(R.id.myEdit); int monAge = Integer.parseInt( monEdit.getText().toString() ); TextView monTexte = (TextView) findViewById(R.id.myText); String text = "Vous avez " + monAge + " ans."; text += majorite.check(monAge); text += retraite.check(monAge); monTexte.setText(text); }

Grace à notre interface, les 2 classes ont en fait un type commun : CheckAgeInterface, on peut donc stocker nos 2 instances différentes dans une variable du même type.

On peut remplacer leur initialisation par:

@Override public void onClick(View v) { CheckAgeInterface majorite = new CheckMajorite(); CheckAgeInterface retraite = new CheckRetraite(); EditText monEdit = (EditText) findViewById(R.id.myEdit); int monAge = Integer.parseInt( monEdit.getText().toString() ); TextView monTexte = (TextView) findViewById(R.id.myText); String text = "Vous avez " + monAge + " ans."; text += majorite.check(monAge); text += retraite.check(monAge); monTexte.setText(text); }

...Et que peut on faire avec plusieurs éléments du même type ?

@Override public void onClick(View v) { CheckAgeInterface[] verifs = {new CheckMajorite(), new CheckRetraite()}; EditText monEdit = (EditText) findViewById(R.id.myEdit); int monAge = Integer.parseInt( monEdit.getText().toString() ); TextView monTexte = (TextView) findViewById(R.id.myText); String text = "Vous avez " + monAge + " ans."; for(int i=0; i < verifs.length; i++) { text += verifs[i].check(monAge); } monTexte.setText(text); }

Grâce à cela, le nombre de modification nécessaire pour ajouter une fonctionnalité est encore réduit.

Il suffirait de créer une nouvelle classe, et de simplement l'instancier à la suite des autres, dans le tableau.

Niveau 3 de l'orienté objet : l'injection de dépendance.

Il reste encore une partie de notre code qui n'est pas "orienté objet" : la gestion de l'affichage.

Les interfaces permettent de regrouper certains objets comme l'on vient de voir, mais elle permettent aussi de définir un contrat.

Je passe mon objet (ma MainActivity par exemple) non pas en tant que MainActivity, mais en tant que l'interface que j'ai choisi, et qui est attendue par l'autre objet. L'autre objet n'a pas besoin de connaitre quoi que ce soit de mon Activity, à part qu'il possède bien la ou les méthodes qu'il attends.

Elle permettent de passer un objet à un autre sans qu'il ne le connaisse vraiment : on appelle cela l'injection de dépendance

C'est typiquement le cas du système d'évenement d'Android.

L'objet Button annonce : "J'attends un objet de type OnClickListener et j'appellerai la méthode onClick sur cet objet, quel qu'il soit en réalité, dès que l'utilisateur aura cliqué le bouton".

De la même maniere, les objets CheckMajorite et CheckRetraite annoncent : "On attends un objet de type CheckAgeListenerInterface et on appelera la méthode ageChecked en lui passant le bon message dès qu'on aura vérifié ses droits.

package fr.cnam.premierprogramme; public interface CheckAgeListenerInterface { public void ageChecked(boolean result, String message); }

package fr.cnam.premierprogramme; public interface CheckAgeInterface { public void check(int age, CheckAgeListenerInterface listener); }

package fr.cnam.premierprogramme; public class CheckMajorite implements CheckAgeInterface { public void check(int age, CheckAgeListenerInterface listener){ if (age >= 18) { listener.ageChecked(true, " vous etes majeur "); } else { listener.ageChecked(false, " vous etes mineur "); } } }

package fr.cnam.premierprogramme; public class CheckRetraite implements CheckAgeInterface { public void check(int age, CheckAgeListenerInterface listener){ if (age >= 62) { listener.ageChecked(true, " et vous pouvez prendre votre retraite."); } else { listener.ageChecked(false, " et vous devez encore travailler, desole."); } } }

public class MainActivity extends Activity implements OnClickListener, CheckAgeListenerInterface { ... @Override public void onClick(View v) { CheckAgeInterface[] verifs = {new CheckMajorite(), new CheckRetraite()}; EditText monEdit = (EditText) findViewById(R.id.myEdit); int monAge = Integer.parseInt( monEdit.getText().toString() ); TextView monTexte = (TextView) findViewById(R.id.myText); monTexte.setText("Vous avez " + monAge + " ans."); for(int i=0; i < verifs.length; i++) { verifs[i].check(monAge, this); } } public void ageChecked(boolean result, String message){ TextView monTexte = (TextView) findViewById(R.id.myText); String actuel = monTexte.getText().toString(); monTexte.setText(actuel + message); } }

L'interêt d'un tel découpage, c'est que l'on peut encore séparer les responsabilités : je peux facilement sortir la responsabilité du traitement du message dans des classes séparées.

L'avantage des interfaces c'est de laisser le choix maximal à l'utilisateur: Je peux créer une classe pour l'occasion ou réutiliser une classe existante (comme ici MainActivity)

Je peux par exemple créer un nouveau mode d'affichage :

package fr.cnam.premierprogramme; import android.app.Activity; import android.widget.Toast; public class ToastCheckAgeListener implements CheckAgeListenerInterface { Activity activity; public ToastCheckAgeListener(Activity activity) { this.activity = activity; } @Override public void ageChecked(boolean result, String message) { Toast toast = Toast.makeText(this.activity, message, Toast.LENGTH_LONG); toast.show(); } }

et en modifiant à peine mon Activity :

@Override public void onClick(View v) { ToastCheckAgeListener listener = new ToastCheckAgeListener(this); CheckAgeInterface[] verifs = {new CheckMajorite(), new CheckRetraite()}; EditText monEdit = (EditText) findViewById(R.id.myEdit); int monAge = Integer.parseInt( monEdit.getText().toString() ); TextView monTexte = (TextView) findViewById(R.id.myText); monTexte.setText("Vous avez " + monAge + " ans."); for(int i=0; i < verifs.length; i++) { verifs[i].check(monAge, listener); } }

L'orienté objet : modulariser pour prévoir le changement

A travers ces exemples, nous avons :

Ces principes appliqués permettent :

Un mot sur les tests unitaires

Les tests unitaires sont des classes qui ne seront pas utilisées directement lors de l'execution du programme, mais "seulement" pendant son développement.

Idéalement, chaque classe "normale" doit avoir sa classe associée de test unitaire. Ces tests permettent de s'assurer que la classe fonctionne comme prévu, selon les parametres qu'on lui passe.

A chaque modification de l'application, on relance la totalité des tests unitaires. Si un bug s'est introduit lors d'une modification, on a des chances de le trouver à ce moment la.

On ne peut faire des tests unitaires efficaces que si les classes sont clairement découpées.

Dans la réalité, les tests unitaires prennent beaucoup de temps de développement. On sélectionnera donc que certaines classes à tester. Nos classes "Check..." serait de bonnes candidates au test car elle des responsabilités très nettes, contrairement à la MainActivity par exemple.

Il existe une méthode de travail qui encourage de commencer par la rédaction des tests, et ensuite seulement la rédaction de la classe qui y répond. C'est le TDD (Test-Driven-Developement).

Exercice :

Version 1

Réaliser l'application "VosDroits"

Les droits à vérifier sont : majorité en france, âge de départ à la retraite, carte réduction SNCF (entre 12 et 25 ans), et au moins un autre de votre choix.

Version 2

Modifier le mode d'affichage : utiliser les "Toast" ET l'affichage classique.

Version 3

Agrandir le formulaire avec 2 autres informations : le sexe (bouton radio) et le nombre d'enfants.

Modifier les vérifications existante pour prendre en compte toutes ces informations : à vous de trouver quel sera le moyen le plus évolutif.

Ajouter une vérification qui utilise une de ces information (droit aux allocations familiales à partir de 2 enfants par exemple).